/**
 * List filters are able to be preloaded/backed by an Ext.data.Store to load
 * their options the first time they are shown.
 *
 * List filters are also able to create their own list of values from  all unique values of
 * the specified {@link #dataIndex} field in the store at first time of filter invocation.
 *
 * Example Usage:
 *
 *     var filters = Ext.create('Ext.grid.Panel', {
 *         ...
 *         columns: [{
 *             text: 'Size',
 *             dataIndex: 'size',
 *
 *             filter: {
 *                 type: 'list',
 *                 // options will be used as data to implicitly creates an ArrayStore
 *                 options: ['extra small', 'small', 'medium', 'large', 'extra large']
 *             }
 *         }],
 *         ...
 *     });
 */
Ext.define('Ext.grid.filters.filter.List', {
    extend: 'Ext.grid.filters.filter.SingleFilter',
    alias: 'grid.filter.list',

    type: 'list',

    operator: 'in',

    itemDefaults: {
        checked: false,
        hideOnClick: false
    },

    /**
     * @cfg {Array} [options]
     * `data` to be used to implicitly create a data store
     * to back this list when the data source is **local**. If the
     * data for the list is remote, use the {@link #store}
     * config instead.
     *
     * If neither store nor {@link #options} is specified, then the choices list is automatically
     * populated from all unique values of the specified {@link #dataIndex} field in the store at first
     * time of filter invocation.
     *
     * Each item within the provided array may be in one of the
     * following formats:
     *
     *   - **Array** :
     *
     *         options: [
     *             [11, 'extra small'],
     *             [18, 'small'],
     *             [22, 'medium'],
     *             [35, 'large'],
     *             [44, 'extra large']
     *         ]
     *
     *   - **Object** :
     *
     *         labelField: 'name', // override default of 'text'
     *         options: [
     *             {id: 11, name:'extra small'},
     *             {id: 18, name:'small'},
     *             {id: 22, name:'medium'},
     *             {id: 35, name:'large'},
     *             {id: 44, name:'extra large'}
     *         ]
     *
     *   - **String** :
     *
     *         options: ['extra small', 'small', 'medium', 'large', 'extra large']
     *
     */

    /**
     * @cfg {String} idField
     * Defaults to 'id'.
     */
    idField: 'id',

    /**
     * @cfg {String} labelField
     * Defaults to 'text'.
     */
    labelField: 'text',

    /**
     * @cfg {String} paramPrefix
     * Defaults to 'Loading...'.
     */
    loadingText: 'Loading...',

    /**
     * @cfg {Boolean} loadOnShow
     * Defaults to true.
     */
    loadOnShow: true,

    /**
     * @cfg {Boolean} single
     * Specify true to group all items in this list into a single-select
     * radio button group. Defaults to false.
     */
    single: false,

    plain: true,

    /**
     * @cfg {Ext.data.Store} [store]
     * The {@link Ext.data.Store} this list should use as its data source.
     *
     * If neither store nor {@link #options} is specified, then the choices list is automatically
     * populated from all unique values of the specified {@link #dataIndex} field in the store at first
     * time of filter invocation.
     */

    destroy: function () {
        var me = this,
            store = me.store;

        // We may bind listeners to both the options store & grid store, so we
        // need to unbind both sets here
        if (store) {
            if (me.autoStore) {
                store.destroy();
            } else {
                store.un('load', me.createMenuStore, me);
            }
            me.store = null;
        }

        store = me.getGridStore();
        if (store) {
            store.un('load', me.createMenuStore, me);

            if (me.autoGeneratedOptions) {
                me.autoGeneratedOptions = null;

                store.un({
                    scope: me,
                    datachanged: me.onDataChanged,
                    update: me.onDataChanged
                });
            }
        }

        me.callParent();
    },

    activateMenu: function () {
        var me = this,
            items = me.menu.items,
            value = me.filter.getValue(),
            i, len, checkItem;

        for (i = 0, len = items.length; i < len; i++) {
            checkItem = items.getAt(i);

            if (value.indexOf(checkItem.value) > -1) {
                checkItem.setChecked(true, /*suppressEvents*/ true);
            }
        }
    },

    getFilterConfig: function (config, key) {
        // List filter needs to have its value set immediately or else could will fail when filtering since its
        // _value would be undefined.
        config.value = this.options || config.value || [];
        return this.callParent([config, key]);
    },

    /**
     * @private
     * Creates the Menu for this filter.
     * @param {Object} config Filter configuration
     * @return {Ext.menu.Menu}
     */
    createMenu: function(config) {
        var me = this,
            gridStore = me.getGridStore(),
            store = me.store,
            options = me.options,
            menu;

        me.callParent([config]);
        menu = me.menu;

        if (me.preprocessed) {
            me.createMenuItems(store || gridStore);
        } else if (store) {
            if (!store.getCount()) {
                menu.add({
                    text: me.loadingText,
                    iconCls: Ext.baseCSSPrefix + 'mask-msg-text'
                });

                // Add a listener that will auto-load the menu store if `loadOnShow` is true (the default).
                // Don't bother with mon here, the menu is destroyed when we are
                menu.on('show', me.show, me);

                store.on('load', me.createMenuStore, me, {single: true});
            } else {
                me.createMenuItems(store);
            }

        }
        // If there are supplied options, then we know the store is local.
        else if (options) {
            me.createMenuStore(options);
        }
        // A ListMenu which is completely unconfigured acquires its store from the unique values of its field in the store.
        // Note that the gridstore may have already been filtered on load if the column filter had been configured as active
        // with no items checked by default.
        else if (gridStore.getCount() || gridStore.data.filtered) {
            me.createMenuStore(gridStore);
        }
        // If there are no records in the grid store, then we know it's async and we need to listen for its 'load' event.
        else {
            gridStore.on('load', me.createMenuStore, me, {single: true});
        }
    },

    /** @private */
    createMenuItems: function (store) {
        var me = this,
            menu = me.menu,
            len = store.getCount(),
            autoGeneratedOptions = me.autoGeneratedOptions,
            disableChecked, listeners, itemDefaults, record, gid,
            itemValue, i, item, value, checkValue;

        // B/c we're listening to datachanged event, we need to make sure there's a menu.
        if (len && menu) {
            value = [];
            listeners = {
                checkchange: me.onCheckChange,
                scope: me
            };
            itemDefaults = me.getItemDefaults();
            disableChecked = me.value && itemDefaults.checked;
            menu.suspendLayouts();
            menu.removeAll(true);
            gid = me.single ? Ext.id() : null;

            // Needed for when options are auto-generated from the grid store.
            // See the comments in #onDataChanged.
            if (autoGeneratedOptions) {
                autoGeneratedOptions.length = 0;
            }

            // If there is a `value` config and a menu item `checked` config then we must temporarily turn off
            // the `checked` config so all the menu items aren't checked.
            if (disableChecked) {
                itemDefaults.checked = false;
            }

            for (i = 0; i < len; i++) {
                record = store.getAt(i);
                itemValue = record.get(me.idField);

                if (autoGeneratedOptions) {
                    autoGeneratedOptions.push(itemValue);
                }

                item = menu.add(Ext.apply({
                    text: record.get(me.labelField),
                    group: gid,
                    value: itemValue,
                    listeners: listeners
                }, itemDefaults));
            }

            // Restore the user config.
            if (disableChecked) {
                itemDefaults.checked = false;
            }

            menu.resumeLayouts(true);

            me.loaded = true;
        }
    },

    createMenuStore: function (options) {
        var me = this,
            idField = me.idField,
            labelField = me.labelField,
            store = me.store,
            i, len, value, o, storeData;

        if (me.grid.isDestroyed) {
            return;
        }

        if (options !== store) {
            if (options.isStore) {
                options = me.getGridStore().collect(me.column.dataIndex, false, true);
            }

            storeData = [];
            for (i = 0, len = options.length; i < len; i++) {
                value = options[i];

                switch (Ext.typeOf(value)) {
                    case 'array':
                        o = {};
                        o[idField] = value[0];
                        o[labelField] = value[1];
                        storeData.push(value);
                        break;
                    case 'object':
                        storeData.push(value);
                        break;
                    default:
                        if (value != null) {
                            o = {};
                            o[idField] = value;
                            o[labelField] = value;
                            storeData.push(o);
                        }
                }
            }

            if (store) {
                store.destroy();
            }

            me.store = new Ext.data.Store({
                fields: [idField, labelField],
                data: storeData
            });

            me.autoStore = true;
        }

        me.createMenuItems(me.store);
        me.loaded = true;
    },

    onCheckChange: function (checkItem, checked) {
        var me = this,
            updateBuffer = me.updateBuffer;

        if (updateBuffer) {
            me.task.delay(updateBuffer, null, null, [me.getValue(checkItem)]);
        } else {
            me.setValue(me.getValue(checkItem));
        }
    },

    onDataChanged: function (store) {
        // If the menu item options (and the options store) are being auto-generated from the grid store, then it
        // needs to know when the grid store has changed its data so it can remain in sync.
        //
        // We need to gather the `autoGeneratedOptions` every time the menu items are created so we can compare values.
        var autoGeneratedOptions = this.autoGeneratedOptions;

        // Note that autoGeneratedOptions won't be populated with values until the menu is shown and the Filter item's
        // items are created.
        if (autoGeneratedOptions) {
            // Get all unique values, including nulls, either from .data or ._source (if filtered) and compare to the
            // unique options gathered when the menu items are instanced.
            if (!Ext.Array.equals(store.collect(this.idField, true, store.data.filtered).sort(), autoGeneratedOptions.sort())) {
                this.createMenuStore(store);
            }
        }
    },

    preprocess: function () {
        // In order to fully support the `active` config, we need to do some preprocessing in case we need to fetch store data
        // in order to create the options menu items.
        //
        // In addition, if the List filter is auto-creating its store from the unique values in the grid store (i.e., no `options` or
        // `store` configs), it will need to listen to grid store events to properly sync its options when the grid store changes.
        var me = this,
            preprocess = me.active && me.itemDefaults.checked,
            store = me.store,
            gridStore = me.getGridStore();

        if (preprocess && store) {
            store.on('load', 'preprocessStoreFilter', me, { single: true });
        }
        // Load the grid store if `store` and `options` aren't defined, we'll need to get the unique options from it.
        else if (preprocess && !me.options) {
            gridStore.on('load', 'preprocessStoreFilter', me, { single: true });
        }

        if (!me.options && !store) {
            // Needed for when options are auto-generated from the grid store.
            // See the comments in #onDataChanged.
            me.autoGeneratedOptions = [];

            gridStore.on({
                datachanged: { fn: 'onDataChanged', scope: me},
                update: { fn: 'onDataChanged', scope: me}
            });
        }
    },

    preprocessStoreFilter: function (store, records, successful) {
        // If the filter is configured as active with a store or uses the grid store for its options, the values need to
        // be set on the store filter as soon as the list options store is loaded.
        var me = this,
            valueCfg = me.value,
            contains = Ext.Array.contains,
            idField, value, values, i, len;

        if (successful) {
            me.preprocessed = true;
            idField = me.idField;
            values = [];

            for (i = 0, len = records.length; i < len; i++) {
                value = records[i].get(idField);

                // Note that if a `value` config is set, we must only push values from the grid store that are contained within
                // that config onto the array to be set as the store filter value!
                if (valueCfg && !contains(valueCfg, value)) {
                    continue;
                }

                values.push(value);
            }

            me.filter.setValue(values);
            me.updateStoreFilter(values);
        }
    },

    /**
     * @private
     * Template method that is to set the value of the filter.
     */
    setValue: function () {
        var me = this,
            items = me.menu.items,
            value = [],
            i, len, checkItem;

        for (i = 0, len = items.length; i < len; i++) {
            checkItem = items.getAt(i);

            if (checkItem.checked) {
                value.push(checkItem.value);
            }
        }

        //me.selected = value;

        me.filter.setValue(value);
        len = value.length;

        if (len && me.active) {
            me.updateStoreFilter(me.filter);
        } else {
            me.setActive(!!len);
        }
    },

    show: function () {
        var store = this.store;
        if (this.loadOnShow && !this.loaded && !store.hasPendingLoad()) {
            store.load();
        }
    }
});
